#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import copy

from oslo.config import cfg
import six

cfg.CONF.import_opt('instance_user', 'heat.common.config')

from heat.common import exception
from heat.engine import clients
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine.resources.network_interface import NetworkInterface
from heat.engine.resources.neutron import neutron
from heat.engine.resources import nova_utils
from heat.engine.resources import volume
from heat.engine import scheduler
from heat.openstack.common.gettextutils import _
from heat.openstack.common import log as logging

from heat.engine.resources.qingresources import qing_utils
from heat.engine.resources.qingresources import qing_cfg

from time import sleep

logger = logging.getLogger(__name__)

class Instance(resource.Resource):

    PROPERTIES = (
        IMAGE_ID, INSTANCE_TYPE, KEY_NAME, AVAILABILITY_ZONE,
        DISABLE_API_TERMINATION, KERNEL_ID, MONITORING,
        PLACEMENT_GROUP_NAME, PRIVATE_IP_ADDRESS, RAM_DISK_ID,
        SECURITY_GROUPS, SECURITY_GROUP_IDS, NETWORK_INTERFACES,
        SOURCE_DEST_CHECK, SUBNET_ID, TAGS, NOVA_SCHEDULER_HINTS, TENANCY,
        USER_DATA, VOLUMES,REGION,PASSWORD,
    ) = (
        'ImageId', 'InstanceType', 'KeyName', 'AvailabilityZone',
        'DisableApiTermination', 'KernelId', 'Monitoring',
        'PlacementGroupName', 'PrivateIpAddress', 'RamDiskId',
        'SecurityGroups', 'SecurityGroupIds', 'NetworkInterfaces',
        'SourceDestCheck', 'SubnetId', 'Tags', 'NovaSchedulerHints', 'Tenancy',
        'UserData', 'Volumes', 'Region','Password',
    )

    _TAG_KEYS = (
        TAG_KEY, TAG_VALUE,
    ) = (
        'Key', 'Value',
    )

    _NOVA_SCHEDULER_HINT_KEYS = (
        NOVA_SCHEDULER_HINT_KEY, NOVA_SCHEDULER_HINT_VALUE,
    ) = (
        'Key', 'Value',
    )

    _VOLUME_KEYS = (
        VOLUME_DEVICE, VOLUME_ID,
    ) = (
        'Device', 'VolumeId',
    )

    properties_schema = {
        IMAGE_ID: properties.Schema(
            properties.Schema.STRING,
            _('Qing image ID or name.'),
            required=True
        ),
        REGION: properties.Schema(
            properties.Schema.STRING,
            _('Region id'),
            required=True
        ),
        PASSWORD: properties.Schema(
            properties.Schema.STRING,
            _('Root user password'),
            required=False
        ),
        # AWS does not require InstanceType but Heat does because the nova
        # create api call requires a flavor
        INSTANCE_TYPE: properties.Schema(
            properties.Schema.STRING,
            _('Qing instance type (flavor).'),
            required=True,
            update_allowed=True
        ),
        KEY_NAME: properties.Schema(
            properties.Schema.STRING,
            _('Optional Nova keypair name.'),
            constraints=[
                constraints.CustomConstraint("nova.keypair")
            ]
        ),
        AVAILABILITY_ZONE: properties.Schema(
            properties.Schema.STRING,
            _('Availability zone to launch the instance in.')
        ),
        DISABLE_API_TERMINATION: properties.Schema(
            properties.Schema.STRING,
            _('Not Implemented.'),
            implemented=False
        ),
        KERNEL_ID: properties.Schema(
            properties.Schema.STRING,
            _('Not Implemented.'),
            implemented=False
        ),
        MONITORING: properties.Schema(
            properties.Schema.BOOLEAN,
            _('Not Implemented.'),
            implemented=False
        ),
        PLACEMENT_GROUP_NAME: properties.Schema(
            properties.Schema.STRING,
            _('Not Implemented.'),
            implemented=False
        ),
        PRIVATE_IP_ADDRESS: properties.Schema(
            properties.Schema.STRING,
            _('Not Implemented.'),
            implemented=False
        ),
        RAM_DISK_ID: properties.Schema(
            properties.Schema.STRING,
            _('Not Implemented.'),
            implemented=False
        ),
        SECURITY_GROUPS: properties.Schema(
            properties.Schema.LIST,
            _('Security group names to assign.')
        ),
        SECURITY_GROUP_IDS: properties.Schema(
            properties.Schema.LIST,
            _('Security group IDs to assign.')
        ),
        NETWORK_INTERFACES: properties.Schema(
            properties.Schema.LIST,
            _('Network interfaces to associate with instance.'),
            update_allowed=True
        ),
        SOURCE_DEST_CHECK: properties.Schema(
            properties.Schema.BOOLEAN,
            _('Not Implemented.'),
            implemented=False
        ),
        SUBNET_ID: properties.Schema(
            properties.Schema.STRING,
            _('Subnet ID to launch instance in.'),
            update_allowed=True
        ),
        TAGS: properties.Schema(
            properties.Schema.LIST,
            _('Tags to attach to instance.'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    TAG_KEY: properties.Schema(
                        properties.Schema.STRING,
                        required=True
                    ),
                    TAG_VALUE: properties.Schema(
                        properties.Schema.STRING,
                        required=True
                    ),
                },
            ),
            update_allowed=True,
        ),
        NOVA_SCHEDULER_HINTS: properties.Schema(
            properties.Schema.LIST,
            _('Scheduler hints to pass to Nova (Heat extension).'),
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    NOVA_SCHEDULER_HINT_KEY: properties.Schema(
                        properties.Schema.STRING,
                        required=True
                    ),
                    NOVA_SCHEDULER_HINT_VALUE: properties.Schema(
                        properties.Schema.STRING,
                        required=True
                    ),
                },
                
            ),
            implemented=False
        ),
        TENANCY: properties.Schema(
            properties.Schema.STRING,
            _('Not Implemented.'),
            constraints=[
                constraints.AllowedValues(['dedicated', 'default']),
            ],
            implemented=False
        ),
        USER_DATA: properties.Schema(
            properties.Schema.STRING,
            _('User data to pass to instance.')
        ),
        VOLUMES: properties.Schema(
            properties.Schema.LIST,
            _('Volumes to attach to instance.'),
            default=[],
            schema=properties.Schema(
                properties.Schema.MAP,
                schema={
                    VOLUME_DEVICE: properties.Schema(
                        properties.Schema.STRING,
                        _('The device where the volume is exposed on the '
                          'instance. This assignment may not be honored and '
                          'it is advised that the path '
                          '/dev/disk/by-id/virtio-<VolumeId> be used '
                          'instead.'),
                        required=True
                    ),
                    VOLUME_ID: properties.Schema(
                        properties.Schema.STRING,
                        _('The ID of the volume to be attached.'),
                        required=True
                    ),
                }
            )
        ),
    }

    attributes_schema = {'AvailabilityZone': _('The Availability Zone where '
                                               'the specified instance is '
                                               'launched.'),
                         'PrivateDnsName': _('Private DNS name of the'
                                             ' specified instance.'),
                         'PublicDnsName': _('Public DNS name of the specified '
                                            'instance.'),
                         'PrivateIp': _('Private IP address of the specified '
                                        'instance.'),
                         'PublicIp': _('Public IP address of the specified '
                                       'instance.')}

    # Server host name limit to 53 characters by due to typical default
    # linux HOST_NAME_MAX of 64, minus the .novalocal appended to the name
    physical_resource_name_limit = 53

    def __init__(self, name, json_snippet, stack):
        super(Instance, self).__init__(name, json_snippet, stack)
        self.ipaddress = None


    def _ipaddress(self):
        '''
        Return the server's IP address, fetching it from Nova if necessary
        '''
        if self.ipaddress is None:
            iaas = qing_utils.CloudIaas()
            rets = iaas._list_instance_ex([self.resource_id])
            if(rets['ret_code'] != 0):
                return '0.0.0.0'

            self.ipaddress = rets['instance_set'][0]['private_ip']

        return self.ipaddress or '0.0.0.0'

    def _resolve_attribute(self, name):
        res = None
        if name == 'AvailabilityZone':
            res = self.properties[self.AVAILABILITY_ZONE]
        elif name in ['PublicIp', 'PrivateIp', 'PublicDnsName',
                      'PrivateDnsName']:
            res = self._ipaddress()

        logger.info(_('%(name)s._resolve_attribute(%(attname)s) == %(res)s'),
                    {'name': self.name,
                     'attname': name,
                     'res': res})
        return unicode(res) if res else None

    def _build_nics(self, network_interfaces,
                    security_groups=None, subnet_id=None):

        nics = None

        if network_interfaces:
            unsorted_nics = []
            for entry in network_interfaces:
                nic = (entry
                       if not isinstance(entry, basestring)
                       else {'NetworkInterfaceId': entry,
                             'DeviceIndex': len(unsorted_nics)})
                unsorted_nics.append(nic)
            sorted_nics = sorted(unsorted_nics,
                                 key=lambda nic: int(nic['DeviceIndex']))
            nics = [{'port-id': nic['NetworkInterfaceId']}
                    for nic in sorted_nics]
        else:
            # if SubnetId property in Instance, ensure subnet exists
            if subnet_id:
                neutronclient = self.neutron()
                network_id = NetworkInterface.network_id_from_subnet_id(
                    neutronclient, subnet_id)
                # if subnet verified, create a port to use this subnet
                # if port is not created explicitly, nova will choose
                # the first subnet in the given network.
                if network_id:
                    fixed_ip = {'subnet_id': subnet_id}
                    props = {
                        'admin_state_up': True,
                        'network_id': network_id,
                        'fixed_ips': [fixed_ip]
                    }

                    if security_groups:
                        props['security_groups'] = \
                            neutron.NeutronResource.get_secgroup_uuids(
                                security_groups, self.neutron())

                    port = neutronclient.create_port({'port': props})['port']
                    nics = [{'port-id': port['id']}]

        return nics

    def _get_security_groups(self):
        security_groups = []
        for key in (self.SECURITY_GROUPS, self.SECURITY_GROUP_IDS):
            if self.properties.get(key) is not None:
                for sg in self.properties.get(key):
                    security_groups.append(sg)
        if not security_groups:
            security_groups = None
        return security_groups

    def _get_nova_metadata(self, properties):
        if properties is None or properties.get(self.TAGS) is None:
            return None

        return dict((tm[self.TAG_KEY], tm[self.TAG_VALUE])
                    for tm in properties[self.TAGS])

    def handle_create(self):
        security_groups = self.properties[self.SECURITY_GROUPS] or None

        userdata = self.properties[self.USER_DATA] or ''
        #flavor = self.properties[self.INSTANCE_TYPE]

        print('------------- 1 ----------------')

        #image_id = qing_utils.get_image_id(self.IMAGE_ID)
        image_id = self.properties[self.IMAGE_ID] or ''
        flavor_id = self.properties[self.INSTANCE_TYPE] or 'c1m1'
        #flavor_id = qing_utils.get_flavor_id('c1m1')
        print(flavor_id)
        
        #we assusme cxmx
        tmp = flavor_id[1:].split('m')
        cpu = int(tmp[0])
        memory = int(tmp[1]) * 1024
        
        ''' 
        nics = self._build_nics(self.properties[self.NETWORK_INTERFACES],
                                security_groups=security_groups,
                                subnet_id=self.properties[self.SUBNET_ID])
        '''
        server = None

        admin_pass = self.properties[self.PASSWORD] or '123TYUasd'

        sub = self.properties[self.SUBNET_ID] or None
        if sub != None:
            subnet = [sub]
        else:
            subnet = None
        print(subnet)
        vm=None
        try:
            access = self.properties[self.ACCESSKEY] or ''
            security = self.properties[self.SECURITYKEY] or ''
            zone = self.properties[self.REGION] or 'gd1'
            iaas = qing_utils.CloudIaas(access, security, zone)
            rets = iaas.create_instance_ex(
                                image_id=image_id,
                                instance_type = None,
                                cpu = cpu,
                                memory = memory,
                                count = 1,
                                instance_name = self.physical_resource_name(),
                                vxnets = [subnet],
                                security_group = security_groups,
                                login_mode = 'passwd',
                                login_keypair = None,
                                login_passwd = admin_pass)
            if(rets[0] != 0):
                print("qing cloud create instance failed")
                print(rets)
                vm=None
                return
    
            vm=rets[1]

        finally:
            # Avoid a race condition where the thread could be cancelled
            # before the ID is stored
            if vm is not None:
                self.resource_id_set(vm[0])

                #write the matadata to mata server
                qing_cfg.QingConfig.write_userdata(vm[0], userdata)                


        return server #scheduler.TaskRunner(self._attach_volumes_task())

    #TBD: currently we doesn't support attach volume to a instance, all ref info is commented
    def _attach_volumes_task(self):
        return
        iaas = qing_utils.CloudIaas()
        attach_tasks = (iaas.attach_vol(volume_id, self.resource_id) for  volume_id, device in self.volumes())
        return scheduler.PollingTaskGroup(attach_tasks)

    #TBD:
    def check_create_complete(self, cookie):
        return self._check_active(self.resource_id) #and self._check_volume_attached(server, volume_attach_task))

    #TBD:
    def _check_volume_attached(self, server, volume_attach_task):
        return True
        if not volume_attach_task.started():
            volume_attach_task.start()
            return volume_attach_task.done()
        else:
            return volume_attach_task.step()

    def _check_active(self, server):
        '''
        check the instance status
        '''
        iaas = qing_utils.CloudIaas()
        #print(server)
        rets = iaas._list_instance_ex([server])
        #print(rets)
        if(rets['ret_code'] != 0):
            #print('check the instance status failed ')
            #print(rets)
            return False

        if rets['instance_set'][0]['status'] not in ['running', 'creating', 'starting']:
            #print('the instance status is wrong')
            #print(rets)
            return False

        return True

    def volumes(self):
        """
        Return an iterator over (volume_id, device) tuples for all volumes
        that should be attached to this instance.
        """
        volumes = self.properties[self.VOLUMES]

        return ((vol[self.VOLUME_ID],
                 vol[self.VOLUME_DEVICE]) for vol in volumes)

    def _remove_matched_ifaces(self, old_network_ifaces, new_network_ifaces):
        # find matches and remove them from old and new ifaces
        old_network_ifaces_copy = copy.deepcopy(old_network_ifaces)
        for iface in old_network_ifaces_copy:
            if iface in new_network_ifaces:
                new_network_ifaces.remove(iface)
                old_network_ifaces.remove(iface)

    #TBD: update is not support yet
    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        return
        if 'Metadata' in tmpl_diff:
            self.metadata_set(tmpl_diff['Metadata'])
        checkers = []
        server = None
        if self.TAGS in prop_diff:
            server = self.nova().servers.get(self.resource_id)
            nova_utils.meta_update(self.nova(),
                                   server,
                                   self._get_nova_metadata(prop_diff))

        if self.INSTANCE_TYPE in prop_diff:
            flavor = prop_diff[self.INSTANCE_TYPE]
            flavor_id = nova_utils.get_flavor_id(self.nova(), flavor)
            if not server:
                server = self.nova().servers.get(self.resource_id)
            checker = scheduler.TaskRunner(nova_utils.resize, server, flavor,
                                           flavor_id)
            checkers.append(checker)
        if self.NETWORK_INTERFACES in prop_diff:
            new_network_ifaces = prop_diff.get(self.NETWORK_INTERFACES)
            old_network_ifaces = self.properties.get(self.NETWORK_INTERFACES)
            subnet_id = (
                prop_diff.get(self.SUBNET_ID) or
                self.properties.get(self.SUBNET_ID))
            security_groups = self._get_security_groups()
            if not server:
                server = self.nova().servers.get(self.resource_id)
            # if there is entrys in old_network_ifaces and new_network_ifaces,
            # remove the same entrys from old and new ifaces
            if old_network_ifaces and new_network_ifaces:
                # there are four situations:
                # 1.old includes new, such as: old = 2,3, new = 2
                # 2.new includes old, such as: old = 2,3, new = 1,2,3
                # 3.has overlaps, such as: old = 2,3, new = 1,2
                # 4.different, such as: old = 2,3, new = 1,4
                # detach unmatched ones in old, attach unmatched ones in new
                self._remove_matched_ifaces(old_network_ifaces,
                                            new_network_ifaces)
                if old_network_ifaces:
                    old_nics = self._build_nics(old_network_ifaces)
                    for nic in old_nics:
                        checker = scheduler.TaskRunner(
                            server.interface_detach,
                            nic['port-id'])
                        checkers.append(checker)
                if new_network_ifaces:
                    new_nics = self._build_nics(new_network_ifaces)
                    for nic in new_nics:
                        checker = scheduler.TaskRunner(
                            server.interface_attach,
                            nic['port-id'],
                            None, None)
                        checkers.append(checker)
            # if the interfaces not come from property 'NetworkInterfaces',
            # the situation is somewhat complex, so to detach the old ifaces,
            # and then attach the new ones.
            else:
                interfaces = server.interface_list()
                for iface in interfaces:
                    checker = scheduler.TaskRunner(server.interface_detach,
                                                   iface.port_id)
                    checkers.append(checker)
                nics = self._build_nics(new_network_ifaces,
                                        security_groups=security_groups,
                                        subnet_id=subnet_id)
                # 'SubnetId' property is empty(or None) and
                # 'NetworkInterfaces' property is empty(or None),
                # _build_nics() will return nics = None,we should attach
                # first free port, according to similar behavior during
                # instance creation
                if not nics:
                    checker = scheduler.TaskRunner(server.interface_attach,
                                                   None, None, None)
                    checkers.append(checker)
                else:
                    for nic in nics:
                        checker = scheduler.TaskRunner(
                            server.interface_attach,
                            nic['port-id'], None, None)
                        checkers.append(checker)

        if checkers:
            checkers[0].start()
        return checkers

    def check_update_complete(self, checkers):
        '''Push all checkers to completion in list order.'''
        for checker in checkers:
            if not checker.started():
                checker.start()
            if not checker.step():
                return False
        return True

    def metadata_update(self, new_metadata=None):
        '''
        Refresh the metadata if new_metadata is None
        '''
        if new_metadata is None:
            self.metadata_set(self.parsed_template('Metadata'))

    #TBD:
    def validate(self):
        return
        '''
        Validate any of the provided params
        '''
        res = super(Instance, self).validate()
        if res:
            return res

        # check validity of security groups vs. network interfaces
        security_groups = self._get_security_groups()
        if security_groups and self.properties.get(self.NETWORK_INTERFACES):
            raise exception.ResourcePropertyConflict(
                '/'.join([self.SECURITY_GROUPS, self.SECURITY_GROUP_IDS]),
                self.NETWORK_INTERFACES)


    def _detach_volumes_task(self):
        '''
        Detach volumes from the instance
        '''
        detach_tasks = (volume.VolumeDetachTask(self.stack,
                                                self.resource_id,
                                                volume_id)
                        for volume_id, device in self.volumes())
        return scheduler.PollingTaskGroup(detach_tasks)

    def handle_delete(self):
        '''
        Delete an instance, blocking until it is disposed by OpenStack
        '''
        if self.resource_id is None:
            return

        print('delete instance id is %s' %self.resource_id)
        iaas = qing_utils.CloudIaas()
        rets = iaas.del_instance([self.resource_id])
        if rets[0] == 1400:   #the resource is not ready, try it again.
            while rets[0] == 1400:
                sleep(1)
                rets = iaas.del_instance([self.resource_id])
        elif rets[0] != 0:
            print('delete failed')
            print(rets)
            raise exception.Error(_('Cannot delete %s instance, resource_id not set') %
                                  self.name)

        if rets[0] == 0:
            qing_cfg.QingConfig.remove_userdata(self.resource_id)

        return

    #TBD:
    def check_delete_complete(self, server_delete_task):
        self.resource_id_set(None)
        return True
        # if the resource was already deleted, server_delete_task will be None
        if server_delete_task is None:
            return True
        else:
            return server_delete_task.step()

    #TBD: 
    def handle_suspend(self):
        '''
        Suspend an instance - note we do not wait for the SUSPENDED state,
        this is polled for by check_suspend_complete in a similar way to the
        create logic so we can take advantage of coroutines
        '''
        if self.resource_id is None:
            raise exception.Error(_('Cannot suspend %s, resource_id not set') %
                                  self.name)

        iaas = qing_utils.CloudIaas()
        iaas.stop_instance(self.resorce_id)


    def check_suspend_complete(self):
        '''
        check the instance status
        '''
        iaas = qing_utils.CloudIaas()
        #print(server)
        rets = iaas._list_instance_ex(self.resource_id)
        #print(rets)
        if(rets['ret_code'] != 0):
            #print('check the instance status failed ')
            #print(rets)
            return False

        if rets['instance_set'][0]['status'] not in ['stopped']:
            #print('the instance status is wrong')
            #print(rets)
            return False

        return True


    def handle_resume(self):
        '''
        Resume an instance - note we do not wait for the ACTIVE state,
        this is polled for by check_resume_complete in a similar way to the
        create logic so we can take advantage of coroutines
        '''
        if self.resource_id is None:
            raise exception.Error(_('Cannot resume %s, resource_id not set') %
                                  self.name)

        iaas = qing_utils.CloudIaas()
        iaas.start_instance(self.resorce_id)


    def check_resume_complete(self, cookie):
        '''
        check the instance status
        '''
        iaas = qing_utils.CloudIaas()
        #print(server)
        rets = iaas._list_instance_ex(self.resource_id)
        #print(rets)
        if(rets['ret_code'] != 0):
            #print('check the instance status failed ')
            #print(rets)
            return False

        if rets['instance_set'][0]['status'] not in ['running']:
            #print('the instance status is wrong')
            #print(rets)
            return False

        return True

def resource_mapping():
    return {
        'Heat::Qing::Instance': Instance,
    }
